<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>二维码矫正演示</title>
  <script src="../../plugins/jsfeat-min.js"></script>
  <script src="../../plugins/jsQR.js"></script>

  <style>
    canvas {
      border: 1px solid #ccc;
      margin: 10px;
    }

    #container {
      display: flex;
      gap: 20px;
    }

    #container div {
      text-align: center;
    }
  </style>
</head>

<body>

  <h2>二维码图像矫正</h2>
  <input type="file" id="fileInput" accept="image/*" />

  <div id="container">
    <div>
      <p>原始图像</p>
      <canvas id="originalCanvas"></canvas>
    </div>
    <div>
      <p>矫正后的二维码</p>
      <canvas id="correctedCanvas"></canvas>
    </div>
  </div>

  <script>
    const fileInput = document.getElementById('fileInput');
    const originalCanvas = document.getElementById('originalCanvas');
    const correctedCanvas = document.getElementById('correctedCanvas');
    const ctxOriginal = originalCanvas.getContext('2d');
    const ctxCorrected = correctedCanvas.getContext('2d');

    fileInput.addEventListener('change', function (e) {
      const file = e.target.files[0];
      if (!file) return;

      const img = new Image();
      const reader = new FileReader();

      reader.onload = function (evt) {
        img.onload = function () {
          originalCanvas.width = img.width;
          originalCanvas.height = img.height;
          ctxOriginal.drawImage(img, 0, 0);

          const imageData = ctxOriginal.getImageData(0, 0, img.width, img.height);
          processImage(imageData);
        };
        img.src = evt.target.result;
      };

      reader.readAsDataURL(file);
    });

    function calculateSharpness (imageData) {
      // 转换为灰度数据
      const grayData = new Uint8Array(imageData.width * imageData.height);
      for (let i = 0, j = 0; i < imageData.data.length; i += 4, j++) {
        grayData[j] = 0.299 * imageData.data[i] + 0.587 * imageData.data[i + 1] + 0.114 * imageData.data[i + 2];
      }

      // 使用现有laplacian函数
      return laplacian(grayData, imageData.width, imageData.height);
    }

    function laplacian (gray, width, height) {
      let sum = 0, sum2 = 0, N = width * height;
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          let L = 0;
          if (x > 0 && x < width - 1 && y > 0 && y < height - 1) {
            L = -4 * gray[y * width + x]
              + gray[y * width + (x - 1)]
              + gray[y * width + (x + 1)]
              + gray[(y - 1) * width + x]
              + gray[(y + 1) * width + x];
          }
          sum += L;
          sum2 += L * L;
        }
      }
      const mean = sum / N;
      return sum2 / N - mean * mean;
    }
    /**
        * 使用双线性插值对图像进行透视变换
        * @param {ImageData} src - 源图像数据对象，包含像素数据
        * @param {ImageData} dst - 目标图像数据对象，用于存储变换结果
        * @param {Array} transform - 3x3透视变换矩阵(展开为一维数组)
        *
        * 算法说明：
        * 1. 对目标图像每个像素点，通过变换矩阵计算其在源图像中的对应坐标
        * 2. 使用双线性插值从源图像获取像素值
        * 3. 处理边界情况，确保坐标在有效范围内
        */
    function warp_perspective_color (src, dst, transform) {
      const dst_width = dst.width | 0;
      const dst_height = dst.height | 0;
      const src_width = src.width | 0;
      const src_height = src.height | 0;
      const src_data = src.data;
      const dst_data = dst.data;

      // 解构变换矩阵参数
      const [m00, m01, m02, m10, m11, m12, m20, m21, m22] = transform;

      // 预计算边界值
      const src_width_limit = src_width - 1;
      const src_height_limit = src_height - 1;
      const src_stride = src.width * 4;

      for (let i = 0; i < dst_height; ++i) {
        let xs0 = m01 * i + m02;
        let ys0 = m11 * i + m12;
        let ws = m21 * i + m22;

        for (let j = 0; j < dst_width; j++) {
          const sc = 1.0 / ws;
          const xs = xs0 * sc;
          const ys = ys0 * sc;
          const ixs = xs | 0;
          const iys = ys | 0;

          if (xs > 0 && ys > 0 && ixs < src_width_limit && iys < src_height_limit) {
            const a = Math.max(xs - ixs, 0.0);
            const b = Math.max(ys - iys, 0.0);
            const off = (src_stride * iys + (ixs << 2)) | 0;
            const off_bottom = off + src_stride;

            // 使用临时变量减少数组访问
            const r0 = src_data[off];
            const r1 = src_data[off + 4];
            const r2 = src_data[off_bottom];
            const r3 = src_data[off_bottom + 4];

            const g0 = src_data[off + 1];
            const g1 = src_data[off + 5];
            const g2 = src_data[off_bottom + 1];
            const g3 = src_data[off_bottom + 5];

            const b0 = src_data[off + 2];
            const b1 = src_data[off + 6];
            const b2 = src_data[off_bottom + 2];
            const b3 = src_data[off_bottom + 6];

            // 双线性插值计算
            const dst_off = (i * dst.width + j) << 2;
            dst_data[dst_off] = (r0 + a * (r1 - r0)) + b * ((r2 + a * (r3 - r2)) - (r0 + a * (r1 - r0)));
            dst_data[dst_off + 1] = (g0 + a * (g1 - g0)) + b * ((g2 + a * (g3 - g2)) - (g0 + a * (g1 - g0)));
            dst_data[dst_off + 2] = (b0 + a * (b1 - b0)) + b * ((b2 + a * (b3 - b2)) - (b0 + a * (b1 - b0)));
            dst_data[dst_off + 3] = 255;
          }

          xs0 += m00;
          ys0 += m10;
          ws += m20;
        }
      }
    }

    function processImage (imageData) {
      const { data, width, height } = imageData;


      console.time()
      // 检测二维码
      const qrCode = jsQR(data, width, height);
      if (!qrCode) {
        alert("未检测到二维码！");
        return;
      }

      console.log("识别结果：", qrCode.data);

      const { topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner } = qrCode.location;

      // 将检测到的二维码角点作为源点 (从原始图片取点)
      const src_pts = [topLeftCorner, topRightCorner, bottomRightCorner, bottomLeftCorner];
      // padding
      topLeftCorner.x - 5 > 0 ? topLeftCorner.x -= 5 : (topLeftCorner.x = 0);
      topLeftCorner.y - 5 > 0 ? topLeftCorner.y -= 5 : (topLeftCorner.y = 0);
      topRightCorner.x += 5
      topRightCorner.y - 5 > 0 ? topRightCorner.y -= 5 : (topRightCorner.y = 0);
      bottomRightCorner.x += 5
      bottomRightCorner.y += 5
      bottomLeftCorner.x - 5 > 0 ? bottomLeftCorner.x -= 5 : (bottomLeftCorner.x = 0);
      bottomLeftCorner.y += 5

      // 计算目标正方形的尺寸，取检测到的二维码边长的平均值
      const width1 = Math.hypot(topRightCorner.x - topLeftCorner.x, topRightCorner.y - topLeftCorner.y);
      const width2 = Math.hypot(bottomRightCorner.x - bottomLeftCorner.x, bottomRightCorner.y - bottomLeftCorner.y);
      const height1 = Math.hypot(bottomLeftCorner.x - topLeftCorner.x, bottomLeftCorner.y - topLeftCorner.y);
      const height2 = Math.hypot(bottomRightCorner.x - topRightCorner.x, bottomRightCorner.y - topRightCorner.y);

      const targetSize = Math.round(((width1 + width2 + height1 + height2) / 4));

      // 定义目标正方形的角点 (矫正后图片放点的位置)
      const dst_pts = [
        { x: 0, y: 0 },
        { x: targetSize, y: 0 },
        { x: targetSize, y: targetSize },
        { x: 0, y: targetSize }
      ];

      console.log('Source points:', qrCode.location, src_pts);
      console.log('Destination points:', dst_pts);
      console.log('Target size:', targetSize);



      // --- 计算透视变换矩阵 ---
      // 使用 jsfeat 计算从源点到目标点的正向变换矩阵 (src -> dst)
      const forward_warp_mat = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t);
      jsfeat.math.perspective_4point_transform(
        forward_warp_mat,
        src_pts[0].x, src_pts[0].y, dst_pts[0].x, dst_pts[0].y,
        src_pts[1].x, src_pts[1].y, dst_pts[1].x, dst_pts[1].y,
        src_pts[2].x, src_pts[2].y, dst_pts[2].x, dst_pts[2].y,
        src_pts[3].x, src_pts[3].y, dst_pts[3].x, dst_pts[3].y,
      );

      // warp_perspective_color 函数需要从目标到源的逆变换矩阵
      // 使用 jsfeat 计算正向矩阵的逆矩阵
      const inverse_warp_mat = new jsfeat.matrix_t(3, 3, jsfeat.F32_t | jsfeat.C1_t);
      jsfeat.matmath.invert_3x3(forward_warp_mat, inverse_warp_mat);

      console.log('inverse_warp_mat', inverse_warp_mat);


      // 检查逆矩阵是否计算成功
      if (inverse_warp_mat.data[8] === 0) {
        console.error("Could not invert the perspective matrix.");
        alert("无法计算矫正矩阵！(Could not calculate correction matrix!)");
        console.timeEnd("QR code processing");
        return;
      }


      // --- 执行彩色透视变换 ---
      // 创建用于存放矫正后图像数据的 ImageData 对象
      const correctedImageData = ctxCorrected.createImageData(targetSize, targetSize);

      // 调用自定义的彩色透视变换函数
      // 传入原始彩色 ImageData (作为源), 新创建的空白 ImageData (作为目标),
      // 以及计算出的逆变换矩阵的系数数组 (.data)
      warp_perspective_color(imageData, correctedImageData, inverse_warp_mat.data);


      // 更新矫正后的 Canvas
      correctedCanvas.width = targetSize;
      correctedCanvas.height = targetSize;
      ctxCorrected.putImageData(correctedImageData, 0, 0);


      console.log('calculateSharpness', correctedImageData, calculateSharpness(correctedImageData));


      console.timeEnd()
    }
  </script>

</body>

</html>